﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

// for speed and simplicity this does all its generation based on the syntax tree and not on the semantic model
// there are essentially three phases:
// 1. use a SyntaxReceiver to detect and record uses of the attributes that we will generate code for
// 2. Register each usage, discovering how they are related to each other in the abstract syntax tree, bottom-up
// 3. Process the resulting relationship tree top-down, adding in the extra generated code that we want.
// todo: consider incremental generators if necessary. for now it looks like the generator is run
// only on build which could be sufficient for us
namespace EventStore.SourceGenerators.Messaging {
	[Generator]
	public class MessageSourceGenerator : ISourceGenerator {
		GeneratorExecutionContext _context;

		public void Initialize(GeneratorInitializationContext context) {
			context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
		}

		public void Execute(GeneratorExecutionContext context) {
			try {
				ExecuteImpl(context);
			} catch (Exception ex) {
				// tooling only shows the first line of the exception, so make the most of it
				var stack = ex.StackTrace.Replace("\r\n", " >> ");
				throw new Exception($"{ex.Message}. Stack: {stack}", ex);
			}
		}

		void ExecuteImpl(GeneratorExecutionContext context) {
			_context = context;

			if (context.SyntaxReceiver is not SyntaxReceiver syntaxReceiver) {
				throw new Exception("Unexpected syntax receiver");
			}

			var roots = new HashSet<CompilationUnitSyntax>();
			var members = new MemberTree();
			foreach (var candidate in syntaxReceiver.Candidates) {
				RegisterMember(candidate, members, roots);
			}

			foreach (var node in roots) {
				context.AddSource(
					GenFileName(node),
					SourceText.From(
						GenSource(node, members),
						Encoding.UTF8, SourceHashAlgorithm.Sha256));
			}
		}

		string GenSource(CompilationUnitSyntax node, MemberTree members) => $@"// autogenerated
using System.Threading;

#pragma warning disable CS0108 // Member hides inherited member; missing new keyword
{Transform(node, members).NormalizeWhitespace(indentation: "\t", eol: Environment.NewLine).ToFullString()}
";

		string GenFileName(SyntaxNode node) {
			var path = node.SyntaxTree.FilePath;
			var fileName = Path.GetFileNameWithoutExtension(path);
			return $"{fileName}-{(uint)path.GetHashCode()}.g.cs";
		}

		void RegisterMember(MemberDeclarationSyntax member, MemberTree members, HashSet<CompilationUnitSyntax> roots) {
			if (members.TryGetValue(member.Parent, out var siblings)) {
				siblings.Add(member);
			} else {
				members[member.Parent] = new HashSet<MemberDeclarationSyntax> { member };
				if (member.Parent is MemberDeclarationSyntax mds) {
					RegisterMember(mds, members, roots);
				} else if (member.Parent is CompilationUnitSyntax cus) {
					roots.Add(cus);
				} else {
					throw new Exception($"Unexpected parent: \"{member.Parent.Kind()}\"");
				}
			}
		}

		CompilationUnitSyntax Transform(CompilationUnitSyntax node, MemberTree members) =>
			SyntaxFactory
				.CompilationUnit()
				.AddMembers(TransformChildren(node, members));

		MemberDeclarationSyntax[] TransformChildren(SyntaxNode node, MemberTree members) =>
			members.TryGetValue(node, out var children)
				? children
					.Select(x => Transform(x, members))
					.ToArray()
				: Array.Empty<MemberDeclarationSyntax>();

		MemberDeclarationSyntax Transform(MemberDeclarationSyntax node, MemberTree members) =>
			node switch {
				NamespaceDeclarationSyntax x => Transform(x, members),
				FileScopedNamespaceDeclarationSyntax x => Transform(x, members),
				ClassDeclarationSyntax x => Transform(x, members),
				_ => throw new Exception($"Unexpected MemberDeclarationSyntax syntax: \"{node.Kind()}\"")
			};

		NamespaceDeclarationSyntax Transform(NamespaceDeclarationSyntax node, MemberTree members) =>
			SyntaxFactory
				.NamespaceDeclaration(
					name: node.Name,
					externs: default,
					usings: node.Usings,
					members: default)
				.AddMembers(TransformChildren(node, members));

		FileScopedNamespaceDeclarationSyntax Transform(FileScopedNamespaceDeclarationSyntax node, MemberTree members) =>
			SyntaxFactory
				.FileScopedNamespaceDeclaration(
					attributeLists: default,
					modifiers: default,
					name: node.Name,
					externs: default,
					usings: node.Usings,
					members: default)
				.AddMembers(TransformChildren(node, members));

		ClassDeclarationSyntax Transform(ClassDeclarationSyntax node, MemberTree members) {
			if (!node.Modifiers.Any(SyntaxKind.PartialKeyword))
				_context.ReportImpartialMessage(node);

			return SyntaxFactory
				.ClassDeclaration(
					attributeLists: default,
					modifiers: node.Modifiers,
					identifier: node.Identifier,
					typeParameterList: node.TypeParameterList,
					baseList: default,
					constraintClauses: default,
					members: default)
				.AddMembers(TransformChildren(node, members))
				.AddGeneratedMembers(_context, node)
				.WithoutTrivia();
		}

		class SyntaxReceiver : ISyntaxReceiver {
			public List<MemberDeclarationSyntax> Candidates { get; } = new();

			public void OnVisitSyntaxNode(SyntaxNode syntaxNode) {
				if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax &&
					(classDeclarationSyntax.TryGetDerivedMessageAttribute(out _) ||
					 classDeclarationSyntax.TryGetBaseMessageAttribute(out _))) {

					Candidates.Add(classDeclarationSyntax);
				}
			}
		}

		class MemberTree : Dictionary<SyntaxNode, HashSet<MemberDeclarationSyntax>> {
		}
	}
}
